Skip to main content

Database Adapter Pattern

SvelteBolt uses a database adapter pattern to provide a consistent API for database operations across different database providers. This abstraction allows you to switch between databases (Supabase, PocketBase, etc.) without changing your application code.

How It Works​

The adapter pattern provides a unified interface for database operations, regardless of which database you're using behind the scenes.

Database Adapter Interface​

// src/lib/server/database/types.ts
export interface DatabaseAdapter {
auth: {
signInWithOtp: (data: any) => Promise<any>;
signInWithOAuth: (provider: any, options?: any) => Promise<any>;
signInWithPassword: (email: string, password: string) => Promise<any>;
signOut: () => Promise<any>;
resetPasswordForEmail: (email: string, options?: any) => Promise<any>;
updatePassword: (new_password: string) => Promise<any>;
signUp: (email: string, password: string) => Promise<any>;
};
storage: {
fetchAvatarUrl: (userId: string) => Promise<string | null>;
uploadAvatar: (userId: string, file: File) => Promise<any>;
};
data: {
create: (collection: string, data: any) => Promise<any>;
readByColumn: (collection: string, query: any, column: string, eq: string) => Promise<any>;
readById: (collection: string, query: any, id: string) => Promise<any>;
read: (collection: string, query: any) => Promise<any>;
update: (collection: string, id: string, data: any) => Promise<any>;
delete: (collection: string, id: string) => Promise<any>;
};
}

Creating the Adapter​

// src/lib/server/database/index.ts
import type { DatabaseAdapter } from './types';
import { SupabaseAdapter } from './supabase';

export function createDatabaseAdapter(locals: App.Locals): DatabaseAdapter {
if ('supabase' in locals) {
return new SupabaseAdapter(locals.supabase);
}
throw new Error('No database adapter found in locals');
}

Using the Database Adapter​

In your SvelteKit routes, you create an adapter instance and use its methods:

// src/routes/products/+page.server.ts
import { createDatabaseAdapter } from '$lib/server/database';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
const db = createDatabaseAdapter(locals);

// Use the adapter's methods
const products = await db.data.read('products', '*');

return {
products
};
};

Available Methods​

Data Operations​

// Create a new record
const product = await db.data.create('products', {
name: 'iPhone',
price: 999
});

// Read all records
const products = await db.data.read('products', '*');

// Read by ID
const product = await db.data.readById('products', '*', 'product-id');

// Read by column
const userProducts = await db.data.readByColumn('products', '*', 'user_id', userId);

// Update record
const updated = await db.data.update('products', 'product-id', {
price: 899
});

// Delete record
await db.data.delete('products', 'product-id');

Authentication Operations​

// Sign up
await db.auth.signUp('[email protected]', 'password');

// Sign in
await db.auth.signInWithPassword('[email protected]', 'password');

// OAuth sign in
await db.auth.signInWithOAuth('google', { redirectTo: '/dashboard' });

// Password reset
await db.auth.resetPasswordForEmail('[email protected]');

// Sign out
await db.auth.signOut();

Storage Operations​

// Upload avatar
await db.storage.uploadAvatar(userId, file);

// Get avatar URL
const avatarUrl = await db.storage.fetchAvatarUrl(userId);

Benefits of the Adapter Pattern​

1. Database Agnostic​

Your application code doesn't need to know which database you're using. You can switch from Supabase to PocketBase by just changing the adapter implementation.

2. Consistent API​

All database operations use the same method signatures, making your code predictable and easy to understand.

3. Easy Testing​

You can create mock adapters for testing without needing a real database.

4. Type Safety​

The TypeScript interface ensures you're using the correct method signatures and return types.

Example: Supabase Adapter Implementation​

// src/lib/server/database/supabase.ts
import type { DatabaseAdapter } from './types';
import type { SupabaseClient } from '@supabase/supabase-js';

export class SupabaseAdapter implements DatabaseAdapter {
constructor(private supabase: SupabaseClient) {}

data = {
create: async (collection: string, data: any) => {
const { data: result, error } = await this.supabase
.from(collection)
.insert(data)
.single();
if (error) throw error;
return result;
},

read: async (collection: string, query: any) => {
const { data, error } = await this.supabase
.from(collection)
.select(query);
if (error) throw error;
return data;
},

readById: async (collection: string, query: any, id: string) => {
const { data, error } = await this.supabase
.from(collection)
.select(query)
.eq('id', id)
.single();
if (error) throw error;
return data;
},

// ... other methods
};

auth = {
signUp: async (email: string, password: string) => {
return this.supabase.auth.signUp({ email, password });
},

signInWithPassword: async (email: string, password: string) => {
return this.supabase.auth.signInWithPassword({ email, password });
},

// ... other auth methods
};

storage = {
fetchAvatarUrl: async (userId: string) => {
// Implementation specific to Supabase Storage
},

uploadAvatar: async (userId: string, file: File) => {
// Implementation specific to Supabase Storage
}
};
}

Creating Custom Adapters​

You can create adapters for other databases by implementing the DatabaseAdapter interface:

// src/lib/server/database/pocketbase.ts
export class PocketbaseAdapter implements DatabaseAdapter {
constructor(private pb: PocketBase) {}

data = {
create: async (collection: string, data: any) => {
return await this.pb.collection(collection).create(data);
},

read: async (collection: string, query: any) => {
return await this.pb.collection(collection).getFullList();
},

// ... implement all required methods
};

// ... implement auth and storage
}

Then update the factory function:

// src/lib/server/database/index.ts
export function createDatabaseAdapter(locals: App.Locals): DatabaseAdapter {
if ('supabase' in locals) {
return new SupabaseAdapter(locals.supabase);
}
if ('pocketbase' in locals) {
return new PocketbaseAdapter(locals.pocketbase);
}
throw new Error('No database adapter found in locals');
}

Best Practices​

1. Keep It Simple​

The adapter should provide basic CRUD operations. Complex business logic belongs in service layers.

2. Error Handling​

Always handle database errors consistently across adapters.

3. Type Safety​

Use TypeScript interfaces to ensure all adapters implement the same methods.

4. Testing​

Create mock adapters for unit testing:

export class MockAdapter implements DatabaseAdapter {
private data: Map<string, any[]> = new Map();

data = {
create: async (collection: string, data: any) => {
const items = this.data.get(collection) || [];
const newItem = { id: 'mock-id', ...data };
items.push(newItem);
this.data.set(collection, items);
return newItem;
},
// ... other mock implementations
};
}

The adapter pattern is what makes SvelteBolt flexible and database-agnostic while keeping your application code clean and consistent.